// Copyright (c) 2010 Manuel Peinado Gallego <manuel.peinado@gmail.com>
// Distributed under the MIT license

#include "stdafx.h"
#include "WorldBuilder.h"
#include "Plane.h"
#include "TranslationGizmo.h"
#include "GizmoManager.h"
#include "Color.h"
#include "Line.h"
#include "Seg.h"

namespace
{

double rayLineDistance(const Ray & ray, const Line & line, 
                       std::pair<gx::Vec3,gx::Vec3> & closestPnts);

bool constrainPoint(std::vector<gx::Vec3> & planeNormals, 
                    const gx::Vec3 & planeOrigin, const Ray & ray, 
                    gx::Vec3 & dest);
}

// ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-

TranslationGizmo::TranslationGizmo(GizmoManager* manager)
: Gizmo(manager)
{
}

void TranslationGizmo::onNodeDrag(const ScreenPoint & dragPoint, 
                                  gx::Mat4 & localTransform,
                                  gx::Mat4 & gizmoTransform,
                                  NodeEvent::Property& prop)
{
    prop = NodeEvent::NodePosition;

    View3d* view = m_manager->getView();
    Transformable* item = m_manager->getDraggedItem();

    gx::Vec3 shapeCenter = m_iniGlobalTransform.translation;
    ScreenPoint shapeCntrScr = m_manager->getView()->project(shapeCenter);

    ScreenPoint dragDelta = dragPoint - m_clickPoint;

    gx::Mat4 parentInverse(gx::eye);
    if(Node* node = dynamic_cast<Node*>(item))
    {
        gx::Mat4 parentTransform = node->getParentGlobalTransform();
        parentInverse = gx::Mat4(parentTransform).invert();
    }

    Ray pickRay = view->computePickRay(shapeCntrScr + dragDelta);
    std::vector<gx::Vec3> uncontrolledAxes = getUncontrolledAxes();

    gx::Vec3 intersection;
    if(constrainPoint(uncontrolledAxes, shapeCenter, pickRay, intersection))
    {
        gx::Vec3 worldDelta = intersection - shapeCenter;
        gx::Vec3 localDelta = parentInverse * gx::Dir(worldDelta);
        localTransform = gx::Mat4().make_transl(localDelta);

        gizmoTransform = gx::Mat4().make_transl(worldDelta);
    }
    else
    {
        gizmoTransform = localTransform = gx::eye;
    }
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -  

std::vector<gx::Vec3> TranslationGizmo::getUncontrolledAxes() const
{
    // TODO use getGizmoMatrix() here if you plan the gizmo pivot or frame to
    // change in the middle of a drag cycle
    gx::Mat4 gizmoFrame = m_iniGizmoFrame;

    std::vector<gx::Vec3> result;
    for(int d = Dims::X; d <= Dims::Z; ++d)
    {
        if(!isDimSelected(Dim(d)))
        {
            result.push_back(gizmoFrame.get_col(d));
        }
    }

    // Allow a maximum of 2 controlled axes. In practice this means
    // that if X, Y and Z are selected, we ignore Z.
    // TODO: ignore the axis that's more "useless"
    if(result.size() == 0)
    {
        result.push_back(gizmoFrame.get_col(2));
    }

    return result;
}

// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  -  

void TranslationGizmo::drawGizmoEnds(View3d * view, 
                                     const Color colors[3],
                                     double size) const
{
    glTranslated(0., 0., 1. - size);
    setColorOrPickCode(view, colors[Dims::Z], Dims::Z);
    glutSolidCone(size * .5, size, 10, 10);

    glTranslated(0., 0., -(1. - size));
    
    glRotated(90., 0., 1., 0.);
    glTranslated(0., 0., 1. - size);

    setColorOrPickCode(view, colors[Dims::X], Dims::X);
    glutSolidCone(size * .5, size, 10, 10);

    glTranslated(0., 0., -(1. - size));

    glRotated(-90., 1., 0., 0.);
    glTranslated(0., 0., 1. - size);

    setColorOrPickCode(view, colors[Dims::Y], Dims::Y);
    glutSolidCone(size * .5, size, 10, 10);
    
    glTranslated(0., 0., -(1. - size));
}

// ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-


namespace
{

bool constrainPoint(std::vector<gx::Vec3> & planeNormals, 
                    const gx::Vec3 & planeOrigin, const Ray & ray, 
                    gx::Vec3 & dest)
{
    ASSERT(planeNormals.size() <= 2);
    
    // Only one constraint plane. This is the simplest thing: just
    // compute the intersection of the ray and that plane
    if(planeNormals.size() == 1)
    {
        return rayPlaneIntersection(ray.origin, ray.direction,
            Plane(planeNormals[0], planeOrigin), dest);
    }
    // Two constraint planes. This means that the constraint is 
    // actually the line where the planes intersect (we assume such
    // line exists). We don't expect an intersection between our ray
    // and that line to exists, so we use as result the closest point
    // on the line with respect to the ray
    else
    {
        Line line(planeOrigin, planeNormals[0].cross(planeNormals[1]));
        std::pair<gx::Vec3, gx::Vec3> closestPnts;
        rayLineDistance(ray, line, closestPnts);
        dest = closestPnts.second;
        return true;
    }
}

double rayLineDistance(const Ray & ray, const Line & line, 
    std::pair<gx::Vec3,gx::Vec3> & closestPnts)
{
    Seg seg0(ray.origin, ray.direction * 1.e10);
    Seg seg1(line.origin - line.direction * 1.e10, 
             line.origin + line.direction * 1.e10);
    return std::sqrt(seg0.dist2(seg1, closestPnts));
}

}

// ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-
